using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using System.Threading;
using System.Runtime.Serialization.Formatters.Binary;
using System.Net;

using RemoteObject;

namespace RemoteClient
{
    public class MyClient
    {
        public static string IPAddress;
        static String RemoteServerUrl = "ServiceURLTcp";
        static int BlockSize = 1024 * 1024;
        static String CloudServerURL = System.Configuration.ConfigurationManager.AppSettings[RemoteServerUrl];

        [Serializable]
        public class BlockInfo
        {
            public int blockID, originPos, currentPos, remainDataLength;

        }

        public enum Status
        {
            Working,
            Pause,
            Completed
        }

        //debug use only
        static void PrintProgress(BlockInfo blockinfo)
        {
            double finishedLength = blockinfo.currentPos - blockinfo.originPos;
            double totalLength = blockinfo.remainDataLength + finishedLength;
            double result = finishedLength / totalLength * 100;
            //Console.WriteLine("Block [" + blockinfo.blockID + "] --- " + result.ToString("f1") + " %");
            if (result == 100) Console.WriteLine("Block [" + blockinfo.blockID + "] --- Completed");
        }
        //debug use only


        [STAThread]
        static void Main(string[] args)
        {
            /*
             *  Unit Test For UploadRequestor
            */

            String fnRemote = @"D:\TestCenter\printsprint.colorstore.zip";
            String fnLocal = @"D:\TestCenter\Result.zip";
            IPHostEntry IpEntry = Dns.GetHostEntry(Dns.GetHostName());
            IPAddress = IpEntry.AddressList[0].ToString();

            UploadRequestor upReq = new UploadRequestor(fnRemote, fnLocal);
            Thread thread = new Thread(new ThreadStart(upReq.StartUpload));
            thread.Start();
            Thread.Sleep(100);


            for (int i = 0; i < 5; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine("The Upload Speed is : " + upReq.GetDownloadSpeed() / 1024 / 1024 + "MB/s");
                Console.WriteLine("The upload Progress is : " + upReq.GetProgress() + "%");

            }

            upReq.RestartUpload();
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine("The upload Speed is : " + upReq.GetDownloadSpeed() / 1024 / 1024 + "MB/s");
                Console.WriteLine("The upload Progress is : " + upReq.GetProgress() + "%");
            }
            


            /*
             *  Unit Test For UploadRequestor
            
            
            String fnRemote = @"D:\TestCenter\printsprint.colorstore.zip";
            String fnLocal = @"D:\TestCenter\Result.zip";
            IPHostEntry IpEntry = Dns.GetHostEntry(Dns.GetHostName());
            IPAddress = IpEntry.AddressList[0].ToString();
            DownloadJob downloadJob = new DownloadJob(fnLocal, fnRemote, CloudServerURL);
            Thread thread = new Thread(new ThreadStart(downloadJob.StartDownload));
            thread.Start();
            Thread.Sleep(100);

            Console.WriteLine("The Size of Download Job : " + downloadJob.GetJobName() + " is : " + downloadJob.GetFileSizeInMB() + "MB");
            for (int i = 0; i < 5; i ++ )
            {
                Thread.Sleep(1000);
                Console.WriteLine("The Download Speed is : " + downloadJob.GetDownloadSpeed() / 1024 / 1024 + "MB/s");
                Console.WriteLine("The Download Progress is : " + downloadJob.GetProgress() + "%");

            }
            
            downloadJob.RestartDownload();
            for (int i = 0; i < 100; i++)
            {
                Thread.Sleep(1000);
                Console.WriteLine("The Download Speed is : " + downloadJob.GetDownloadSpeed() / 1024 / 1024 + "MB/s");
                Console.WriteLine("The Download Progress is : " + downloadJob.GetProgress() + "%");
            }
            */



            /*
             *  Unit Test For HeartBeatObserver
             * 
            HeartBeatObserver ob = HeartBeatObserver.getInstance();
            */

            //For Debug
            Console.WriteLine("Press any key to exit!");
            Console.ReadLine();
        }

        public class DownloadJob
        {

            public const int MAX_BLOCKS = 8;
            public List<Downloader> downloaderList = new List<Downloader>();
            public List<BlockInfo> blockInfoList = new List<BlockInfo>();
            public FileStream fs_net;
            public BinaryFormatter bf = new BinaryFormatter();
            private String fnLocal;
            private String fnRemote;
            private int fileLength;
            private Status jobStatus;
            private List<String> serverUrlList;

            public DownloadJob(String fnLocal, String fnRemote, String serverUrl)
            {
                this.fnLocal = fnLocal;
                this.fnRemote = fnRemote;
                this.serverUrlList = new List<string>();
                this.serverUrlList.Add(serverUrl);
                fileLength = 0;
                jobStatus = Status.Pause;

            }

            public void UpdateProgressFile()
            {
                lock(this)
                {
                    if (fs_net != null)
                    {
                        fs_net.SetLength(0);
                        bf.Serialize(fs_net, blockInfoList);
                    }
                }
            }

            public void DownloaderFinish( Downloader _downloader )
            {
                lock (this)
                {
                    int activeDownloaders = 0;
                    foreach (Downloader downloader in downloaderList)
                    {
                        activeDownloaders += downloader.ThreadFinished ? 0 : 1;
                    }
                    if (activeDownloaders == 0)
                    {
                        if (fs_net != null)
                        {
                            fs_net.Close();
                            fs_net = null;
                            bf = null;
                            Console.WriteLine("Download Job Threads all finished ---------------");
                        }
                    }
                }
            }

            private void StartAllDownloaders()
            {
                lock (this)
                {
                    if (downloaderList.Count != 0)
                    {
                        fs_net = new FileStream(fnLocal + ".net", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
                        foreach (Downloader downloader in downloaderList)
                        {
                            downloader.MyThread.Start();
                        }
                    }
                }
            }

            public void StartDownload()
            {
                if (jobStatus == Status.Working || jobStatus == Status.Completed) return;

                jobStatus = Status.Working;
                RemoteObject.MyRemoteObject proxyObjectTcp = (RemoteObject.MyRemoteObject)Activator.GetObject(typeof(RemoteObject.MyRemoteObject), GetServerURL());
                fileLength = (int)proxyObjectTcp.GetFileLength(fnRemote);

                proxyObjectTcp = null;

                //Considering Breakpoint Resuming
                bool isBreakpointResuming = false;

                if (File.Exists(fnLocal + ".net") && File.Exists(fnLocal))
                {
                    FileStream net_fs_temp = new FileStream(fnLocal + ".net", FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                    if (net_fs_temp.Length != 0)
                    {
                        // breakpoint resume
                        isBreakpointResuming = true;
                        Console.WriteLine("Break Point Resuming");
                        BinaryFormatter bf = new BinaryFormatter();
                        blockInfoList = bf.Deserialize(net_fs_temp) as List<BlockInfo>;

                        downloaderList = new List<Downloader>(MAX_BLOCKS);
                        foreach (BlockInfo obj in blockInfoList)
                        {
                            if (obj.remainDataLength > 0)
                            {
                                Downloader downloader = new Downloader(fnRemote, fnLocal, fileLength, obj, this );
                                Thread thread = new Thread(new ThreadStart(downloader.Run));
                                downloader.MyThread = thread;
                            }
                            PrintProgress(obj);
                        }
                        this.StartAllDownloaders();
                    }
                    net_fs_temp.Close();
                }

                if (!isBreakpointResuming)
                {
                    // Net Task Begin
                    Console.WriteLine("New Task Begin");

                    //create the temp local file with full size
                    if(File.Exists(fnLocal))
                    {
                        File.Delete(fnLocal + ".old");
                        File.Move(fnLocal, fnLocal + ".old");
                    }

                    FileStream fs = new FileStream(fnLocal, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite);
                    fs.SetLength(fileLength);
                    fs.Close();

                    File.Delete(fnLocal + ".old");

                    int startPos;
                    int dataLength;
                    int blockLength = (int)(fileLength + MAX_BLOCKS - 1) / MAX_BLOCKS;
                    downloaderList = new List<Downloader>(MAX_BLOCKS);

                    for (int i = 0; i < MAX_BLOCKS; i++)
                    {
                        startPos = blockLength * i;
                        dataLength = Math.Min(blockLength, fileLength - startPos);
                        BlockInfo blockInfo = new BlockInfo();
                        blockInfo.blockID = i;
                        blockInfo.originPos = startPos;
                        blockInfo.currentPos = startPos;
                        blockInfo.remainDataLength = dataLength;

                        blockInfoList.Add(blockInfo);

                        Downloader downloader = new Downloader(fnRemote, fnLocal, fileLength, blockInfo, this);
                        Thread thread = new Thread(new ThreadStart(downloader.Run));
                        downloader.MyThread = thread;
                    }

                    this.StartAllDownloaders();
                }
            }

            public void StopDownload()
            {
                foreach (Downloader downloader in downloaderList)
                {
                    downloader.StopDownload();
                }
                jobStatus = Status.Pause;
            }

            public void RestartDownload()
            {
                StopDownload();
                Thread.Sleep(1000);
                StartDownload();
            }

            public String GetJobName()
            {
                return fnLocal.Substring(fnLocal.LastIndexOf("\\") + 1);
            }

            public float GetProgress()
            {
                float progress = 0;
                foreach (Downloader downloader in downloaderList)
                {
                    progress += downloader.GetProgress();
                }
                progress /= MAX_BLOCKS;
                if (progress == 100) jobStatus = Status.Completed;
                return progress;
            }

            public float GetDownloadSpeed()
            {
                float speed = 0;
                foreach (Downloader downloader in downloaderList)
                {
                    speed += downloader.GetDownloadSpeed();
                }
                return speed;            
            }

            public float GetFileSizeInMB()
            {
                return fileLength/1024.0f/1024.0f;
            }

            public Status GetStatus()
            {
                return jobStatus;
            }

            public List<Downloader> GetDownloaderList()
            {
                return downloaderList;
            }

            public String GetServerURL()
            {
                // TODO: [Vincent] We temporary return teh first element of the List.
                // Would expand the Host List Later.
                return serverUrlList[0];
            }
        }

        public class Downloader
        {
            static Byte[] endingBlock = new Byte[1];

            private String fnRemote;
            private String fnLocal;
            private int fileLength;
            private float downloadBytesPerSec = 0;
            private float downloadSpeed = 0;
            private float progress;
            private int startPos;
            private int endPos;
            private Boolean stopFlag = false;

            public Thread MyThread { get; set; }
            public bool ThreadFinished { get; set; }

            FileStream fs;
            TransferParam param = new TransferParam();
            List<Byte[]> blocksReceived = new List<Byte[]>();
            
            BlockInfo blockInfo;
            DownloadJob ownerJob;

            //debug use only
            double fileWriteTime = 0;
            double totalDownloadTime = 0;
            //debug use only -end

            public Downloader(String remoteFile, String localFile, int fileSize, BlockInfo _blockInfo, DownloadJob _job)
            {
                fnRemote = remoteFile;
                fnLocal = localFile;
                fileLength = fileSize;
                blockInfo = _blockInfo;
                ownerJob = _job;
                ownerJob.downloaderList.Add(this);

                float completedLength = blockInfo.currentPos - blockInfo.originPos;
                float totalLength = blockInfo.remainDataLength + completedLength;
                progress = completedLength / totalLength * 100;
            }

            private void TimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
            {
                downloadSpeed = downloadBytesPerSec;
                downloadBytesPerSec = 0;
                //Console.WriteLine("TimerElapsed::Block [" + blockInfo.blockID + "] --- " + (downloadSpeed / 1024).ToString("f1") + " KB/S");
            }

            private void UpdateProgress()
            {
                ownerJob.UpdateProgressFile();

                float completedLength = blockInfo.currentPos - blockInfo.originPos;
                float totalLength = blockInfo.remainDataLength + completedLength;
                progress = completedLength / totalLength * 100;

                PrintProgress(blockInfo);
            }

            private void WriteFileProc()
            {
                do
                {
                    Thread.Sleep(10);

                    List<Byte[]> blocksReceivedToSave;
                    lock (this)
                    {
                        blocksReceivedToSave = blocksReceived;
                        blocksReceived = new List<Byte[]>();
                    }

                    if (blocksReceivedToSave.Count != 0)
                    {
                        foreach (Byte[] block in blocksReceivedToSave)
                        {
                            if (block == endingBlock)
                            {
                                UpdateProgress();
                                if( fs!= null ) 
                                    fs.Close();
                                blocksReceivedToSave.Clear();
                                ThreadFinished = true;
                                ownerJob.DownloaderFinish(this);
                                System.GC.Collect();
                                return;
                            }
                            else
                            {
                                Byte[] blockToSave = block;
                                if (param.useCompressMode)
                                {
                                    blockToSave = RemoteObject.MyUtility.DeCompression(block);
                                }
                                fs.Write(blockToSave, 0, blockToSave.Length);

                                //after save successful, then update the block info. This is important for broken-resuming
                                blockInfo.currentPos += blockToSave.Length;
                                blockInfo.remainDataLength -= blockToSave.Length;
                            }
                        }
                        UpdateProgress();
                        blocksReceivedToSave.Clear();
                    }

                } while (true);
            }

            public void Run()
            {
                DateTime startTime = DateTime.Now;

                stopFlag = false;

                startPos = blockInfo.currentPos;
                endPos = blockInfo.currentPos + blockInfo.remainDataLength;
                int dataLength = blockInfo.remainDataLength;

                //set transfer param
                param.BlockSize = BlockSize;
                param.BuffSize = BlockSize * 5;
                param.useCompressMode = false;

                try
                {
                    Console.WriteLine(startPos + " --- " + (startPos + dataLength));

                    // create the write file thread anyway
                    Thread thread = new Thread(new ThreadStart(this.WriteFileProc));
                    thread.Start();

                    RemoteObject.MyRemoteObject proxyObjectTcp = null;
                    long clientID = 0;

                    //try to init file transfer
                    try{
                        proxyObjectTcp = (RemoteObject.MyRemoteObject)Activator.GetObject(typeof(RemoteObject.MyRemoteObject), ownerJob.GetServerURL());
                        clientID = proxyObjectTcp.GetNewClientID();
                        proxyObjectTcp.SetTransferParam(clientID, param);
                        proxyObjectTcp.StartDownloadFile(fnRemote, clientID, startPos, dataLength);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine("Network problem ..... ");
                        Console.WriteLine(e);
                        proxyObjectTcp = null;
                    }
                    finally
                    {
                    }

                    if (proxyObjectTcp != null)
                    {
                        System.Timers.Timer SpeedTimer = new System.Timers.Timer();
                        SpeedTimer.Enabled = true;
                        SpeedTimer.Interval = 1000;
                        SpeedTimer.Elapsed += new System.Timers.ElapsedEventHandler(TimerElapsed);

                        try
                        {
                            fs = new FileStream(fnLocal, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
                            fs.Seek(startPos, SeekOrigin.Begin);

                            while (true)
                            {
                                Byte[] receiveData = proxyObjectTcp.RecevingFileData(clientID);
                                if (receiveData == null || receiveData.Length == 0 || stopFlag)
                                    break;
                                lock (this)
                                {
                                    blocksReceived.Add(receiveData);
                                }
                                downloadBytesPerSec += receiveData.Length;
                            }
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(startPos + " --- " + (startPos + dataLength) + " ---- error1 --- ");
                            Console.WriteLine(e);
                        }
                        finally
                        {
                            SpeedTimer.Close();
                            downloadSpeed = 0;
                            proxyObjectTcp.StopDownloadFile(clientID);
                        }
                    }

                }
                catch (Exception e)
                {
                    Console.WriteLine(startPos + " --- " + (startPos + dataLength) + " ---- error2 --- ");
                    Console.WriteLine(e);
                }
                finally
                {
                    //we should always give the endingBlock to let write-file thread close gracefully.
                    lock (this)
                    {
                        blocksReceived.Add(endingBlock);
                    }

                    //debug code
                    TimeSpan ts = DateTime.Now - startTime;
                    totalDownloadTime += ts.TotalMilliseconds;
                    //debug code - end

                    Console.WriteLine(startPos + " --- " + (startPos + dataLength) + " ---- done --- " + fileWriteTime + " ---- duration --- " + totalDownloadTime);

                }
                System.GC.Collect();
            }

            public void StopDownload()
            {
                stopFlag = true;
            }

            public float GetProgress()
            {
                //for debug only
                //Console.WriteLine("Block [" + blockInfo.blockID + "] --- " + progress.ToString("f1") + " %");
                //for debug only
                return progress;
            }

            public float GetDownloadSpeed()
            {
                //for debug only
                //Console.WriteLine("Block [" + blockInfo.blockID + "] --- " + (downloadSpeed / 1024.0f).ToString("f1") + " KB/s");
                //for debug only
                return downloadSpeed;
            }

            public int GetStartPos()
            {
                return startPos;
            }

            public int GetEndPos()
            {
                return endPos;
            }
        }

        public class UploadRequestor
        {
            private string remoteFile;
            private string localFile;
            private RemoteObject.MyRemoteObject robj;
            private float progress;
            private float downloadSpeed;
            private Status jobStatus;
            private long jobID;

            public UploadRequestor(string fnRemote, string fnLocal)
            {
                remoteFile = fnRemote;
                localFile = fnLocal;
                robj = (RemoteObject.MyRemoteObject)Activator.GetObject(typeof(RemoteObject.MyRemoteObject), CloudServerURL);
                progress = 0;
                downloadSpeed = 0;
                jobStatus = Status.Pause;
                jobID = robj.GetNewClientID();
            }

            public void StartUpload()
            {
                if (jobStatus == Status.Working || jobStatus == Status.Completed) return;
                jobStatus = Status.Working;
                robj.StartUploadRequest(remoteFile, localFile, IPAddress, jobID);
                Thread SyncThread = new Thread(new ThreadStart(this.Sync));
                SyncThread.Start();
            }

            public void StopUpload()
            {
                robj.StopUploadRequest(jobID);
                jobStatus = Status.Pause;
            }

            public void RestartUpload()
            {
                StopUpload();
                Thread.Sleep(1000);
                StartUpload();
            }

            public String GetJobName()
            {
                return localFile.Substring(localFile.LastIndexOf("\\") + 1);
            }

            public void Sync()
            {
                do
                {
                    RemoteObject.JobInfo jobinfo = robj.SyncRequest(jobID);
                    progress = jobinfo.progress;
                    downloadSpeed = jobinfo.downloadSpeed;
                    if (progress == 100)
                    {
                        jobStatus = Status.Completed;
                        Thread.CurrentThread.Abort();
                    }
                    Thread.Sleep(1000);
                } while (true);
            }

            public float GetProgress()
            {
                return progress;
            }

            public float GetDownloadSpeed()
            {
                return downloadSpeed;
            }

            public Status GetStatus()
            {
                return jobStatus;
            }

        }

        public class DistributeRequestor
        {
            string remoteFile;
            string localFile;
            RemoteObject.MyRemoteObject robj;
            private long jobID;

            public DistributeRequestor(string fnRemote, string fnLocal)
            {
                remoteFile = fnRemote;
                localFile = fnLocal;
                robj = (RemoteObject.MyRemoteObject)Activator.GetObject(typeof(RemoteObject.MyRemoteObject), CloudServerURL);
                jobID = robj.GetNewClientID();
            }

            public void Run()
            {
                robj.StartUploadRequest(remoteFile, localFile, IPAddress, jobID);
            }
        }

        public class HeartBeatObserver
        {
            private DateTime lastBeatTime;
            private bool isHeartBeatStop;
            private Thread beatingThread;
            private static HeartBeatObserver instance;

            public static HeartBeatObserver getInstance()
            {
                if (instance == null) instance = new HeartBeatObserver();
                return instance;
            }

            private HeartBeatObserver()
            {
                isHeartBeatStop = false;
                beatingThread = new Thread(new ThreadStart(Run));
                beatingThread.Start();
                Thread.Sleep(2000);
                Thread watchThread = new Thread(new ThreadStart(Watch));
                watchThread.Start();
            }

            private void Run()
            {
                RemoteObject.MyRemoteObject proxyObjectTcp = (RemoteObject.MyRemoteObject)Activator.GetObject(typeof(RemoteObject.MyRemoteObject),
                    CloudServerURL);
                do
                {
                    proxyObjectTcp.HeartBeatObserver();
                    lastBeatTime = DateTime.Now;
                    Thread.Sleep(500);
                } while (true);
            }

            private void Watch()
            {
                do
                {
                    if ((DateTime.Now - lastBeatTime).TotalSeconds > 2)
                    {
                        isHeartBeatStop = true;
                    }
                    Console.WriteLine("Debug use, Heartbeat is STOP? Answer is : " + isHeartBeatStop);
                    Thread.Sleep(1000);
                } while (true);
            }

            public bool IsHeartBeatStop()
            {
                return isHeartBeatStop;
            }

        }

    }
}
